fix: signed-shift overflow in hdr_calculate_bucket_config (UBSan, found by fuzzing) - #145
Conversation
ClusterFuzzLite (UBSan) found a signed-left-shift overflow in hdr_calculate_bucket_config: sub_bucket_mask was computed as ((int64_t)sub_bucket_count - 1) << unit_magnitude *before* the (unit_magnitude + sub_bucket_half_count_magnitude > 61) guard that rejects out-of-range configs. A crafted decoded log with lowest_discernible_value ~2^56 (reachable via hdr_log_read -> hdr_decode_compressed_v0 -> hdr_init) shifts 255 by 56 places, which is undefined behaviour for int64_t. Move the guard before the shift so such configs return EINVAL without ever performing the overflowing shift. No change for valid inputs. Found-by: ClusterFuzzLite batch fuzzing (log_reader_fuzzer, UBSan) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review of the sub_bucket_mask shift fix surfaced a sibling signed-overflow in the same validation: 'lowest_discernible_value * 2 > highest_trackable_value' overflows int64_t when a crafted/decoded lowest_discernible_value is near INT64_MAX (reachable via hdr_decode_compressed_v0/v1 -> hdr_init). Rewrite as 'lowest_discernible_value > highest_trackable_value / 2', which is equivalent for all non-overflowing inputs (lowest >= 1 is already checked) and cannot overflow. Verified under UBSan: (INT64_MAX,100) and (2^62+1,100) now return EINVAL with no UB; valid/edge cases unchanged. Found-by: adversarial review of the fuzzing fix (ClusterFuzzLite, UBSan) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t job - test/hdr_histogram_test.c: assert hdr_init rejects (EINVAL, no crash) a config that would overflow the sub_bucket_mask shift (lowest=2^56); this aborts under UBSan on the pre-fix code. - test/regression-bucket-config-shift-overflow.hlog: the fuzzer's crash reproducer, auto-added to the log_reader_fuzzer seed corpus via build.sh. - .github/workflows/ci.yml: new 'sanitizers' job runs the unit suite under ASan+UBSan so this class of bug (integer overflow / OOB) is caught deterministically per-PR, not only in the weekly fuzzing run. Verified the full suite is clean under both sanitizers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b63351d to
e912c8d
Compare
|
🤖 Automated first-pass review — a human maintainer's review is still required before merge. The core fix looks right. Moving the The three Two smaller things. The I read the sources to check the arithmetic equivalence and the recorder ownership — |
…code; enable LeakSanitizer in CI Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…alized read via two-step init) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Without it a missing zlib silently builds the no-op log backend and skips the decode leak tests this PR fixes; ON makes that fail loudly.
The linux legs install a pinned CMake by wget from cmake.org/files. That host is currently returning 503 and intermittently failing the TLS handshake, so the job dies in "Install dependencies" before compiling anything. The merge of #145 went red on build (linux, Debug, x64, minimal, ON) with: Connecting to cmake.org (cmake.org)|66.194.253.25|:443... connected. Unable to establish SSL connection. ##[error]Process completed with exit code 4. Both pinned versions are affected (minimal 3.12.4 and latest 3.17.3), so any linux leg can fail this way; the failure has nothing to do with the code under test. Fetch the identical tarballs from the Kitware GitHub release assets and add wget retries. --no-check-certificate is dropped: it only existed to work around cmake.org's certificate handling, and the release assets present a valid chain. Verified locally for both pinned versions: download, `tar --strip-components 1`, and `cmake/ctest/cpack --version` all match what the matrix expects. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ural_with_offset hdr_init makes two allocations - the counts array and the histogram struct - so free(h) frees only the struct and leaks counts (188416 bytes here). hdr_close frees both, and is what the rest of this file already uses. The sanitizers job added in HdrHistogram#145 enables LeakSanitizer, so this test-only leak now fails CI on this branch; it was written before that job existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…_reference hdr_init makes two allocations - the counts array and the histogram struct - so free(h) frees only the struct and leaks counts (188416 bytes here). hdr_close frees both, and is what the rest of this file already uses. The sanitizers job added in HdrHistogram#145 enables LeakSanitizer, so this test-only leak now fails CI on this branch; it was written before that job existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by fuzzing
The new weekly ClusterFuzzLite batch run (UBSan) flagged undefined behaviour in
hdr_calculate_bucket_config:Root cause
sub_bucket_maskwas computed as((int64_t) sub_bucket_count - 1) << unit_magnitudebefore the guardif (unit_magnitude + sub_bucket_half_count_magnitude > 61) return EINVAL;. A crafted, decoded log entry can producelowest_discernible_value ≈ 2^56→unit_magnitude == 56,sub_bucket_count == 256, so the code shifts255 << 56, which overflowsint64_t— signed-left-shift UB — before the config is rejected.Fix
Move the
> 61guard above thesub_bucket_maskshift, so an out-of-range config returnsEINVALwithout ever performing the overflowing shift. The threshold is unchanged and correct:(sub_bucket_count - 1)occupies(sub_bucket_half_count_magnitude + 1)bits, so the shift's top bit isunit_magnitude + sub_bucket_half_count_magnitude; keeping that≤ 61leaves a representable, non-negativeint64_t. No behavior change for valid inputs.Verification
EINVALpath).🤖 Generated with Claude Code